
local getmetatable = getmetatable
local setmetatable = setmetatable
local module = module
local require = require
local type = type
local error = error
local print = print
local string = string
local math = math

module('fit.Parse')
_VERSION='0.2'

local Parse = require('fit.Parse')

--[[
Throws error if incorrect parameter types are passed.
Throws Parse errors. Format of Parse errors = {message, offset}
--]]
local function findMatchingEndTag(lc, fromHere, tag, offset)
    -- Check parameters
    if type(lc) ~= "string" or type(fromHere) ~= "number" or
       type(tag) ~= "string" or type(offset) ~= "number"
    then
        error("Incorrect parameter type passed:\n" ..
            "findMatchingEndTag(string lc, number fromHere, " ..
            "string tag, number offset")
    end

    count = 1
    startEnd = 0

    while count > 0 do
        embeddedTag = string.find(lc, "<" .. tag, fromHere)
        embeddedTagEnd = string.find(lc, "</" .. tag, fromHere)
        -- Which one is closer?
        if embeddedTag == nil and embeddedTagEnd == nil then
            error({message="Can't find tag: " .. tag, offset=offset})
        end

        if embeddedTag == nil then
            embeddedTag = math.huge
        end
        if embeddedTagEnd == nil then
            embeddedTagEnd = math.huge
        end

        if embeddedTag < embeddedTagEnd then
            count = count + 1
            startEnd = embeddedTag
            fromHere = string.find(lc, ">", embeddedTag) + 1
        elseif embeddedTagEnd < embeddedTag then
            count = count - 1
            startEnd = embeddedTagEnd
            fromHere = string.find(lc, ">", embeddedTagEnd) + 1
        end
    end

    return startEnd
end

local function size(self)
    if self.more == nil then
        return 1
    else
        return self.more:size() + 1
    end
end

local function last(self)
    if self.more == nil then
        return self
    else
        return self.more:last()
    end
end

local function leaf(self)
    if self.parts == nil then
        return self
    else
        return self.parts:leaf()
    end
end

local function at(self, i, j, k)
    if j == nil and k == nil then
        if i == 1 or self.more == nil then
            return self
        else
            return self.more:at(i-1)
        end
    elseif k == nil then
        return self:at(i).parts:at(j)
    else
        return self:at(i, j).parts:at(k)
    end
end

local function removeNonBreakTags(s)
    i,j = string.find(s, "<[^<^>]+>", 1)
    while i ~= nil do
        if string.sub(s, i, j) ~= "<br[%s]+[/]?>" then
            s = string.sub(s, 1, i-1) .. string.sub(s, j+1)
        else
              i = j + 1
        end
        i,j = string.find(s, "<[^<^>]+>", i)
    end

    return s
end

local function unescapeEntities(s)
    s = string.gsub(s, "&lt;", "<")
    s = string.gsub(s, "&gt;", ">")
    s = string.gsub(s, "&nbsp;", " ")
    s = string.gsub(s, "&quot;", "\"")
    s = string.gsub(s, "&amp;", "&")
    return s
end

local function unescape(s)
    s = string.gsub(s, "<br />", "\n")
    s = unescapeEntities(s)
    return s
end

local function normalizeLineBreaks(s)
    s = string.gsub(s, "<%s*br%s*/?%s*>", "<br />")
    s = string.gsub(s, "<%s*/%s*p%s*>%s*<%s*p( .*?)?>", "<br />")
    return s
end

local function condenseWhitespace(s)
    s = string.gsub(s, "%s+", " ")
    s = string.gsub(s, "\160", " ") -- Non-breaking space
    s = string.gsub(s, "&nbsp;", " ")
    s = string.gsub(s, "^%s*(.-)%s*$", "%1")

    return s
end

local function addToTag(self, text)
    self.tag = string.gsub(self.tag, ".$", text .. ">", 1)
end

local function addToBody(self, text)
    if self.body == nil then self.body = "" end
    self.body = self.body .. text
end

-- Print the output HTML page
local function printOut(self, f)
    f:write(self.leader)
    f:write(self.tag)
    if self.parts ~= nil then
        printOut(self.parts, f)
    else
        f:write(self.body)
    end

    f:write(self.tail)

    if self.more ~= nill then
        printOut(self.more, f)
    else
        f:write(self.trailer)
    end
end

do
    local footnoteFiles=0

    local function footnote(self)
        if footnoteFiles >= 25 then
            return "[-]"
        else
            footnoteFiles = footnoteFiles + 1
            thisFootnote = footnoteFiles
            html = "footnotes/" .. thisFootnote .. ".html"
            file = assert(io.open("Reports/" .. html))
            printOut(self, file)
            file:close()
            return "<a href=/fit/Release/Reports/" .. html .. "> [" ..
                thisFootnotea .. "]</a>"
        end
    end
end

local function htmlToText(s)
    s = normalizeLineBreaks(s)
    s = removeNonBreakTags(s)
    s = condenseWhitespace(s)
    s = unescape(s)
    return s
end

local function text(self)
    return htmlToText(self.body)
end

--[[
Parse Constructor

Valid calls to the constructor:
     Parse(String tag, String body, Parse parts, Parse more)
     Parse(String text)
     Parse(String text, String tags[])
     Parse(String text, String tags[], int level, int offset)

Throws Parse errors. Format of Parse errors = {message, offset}
Throws error if incorrect call to constructor is made.
--]]
function __call(self, textOrTag, tagsOrBody, levelOrParts, offsetOrMore)
    local aParse = {}

    -- Instance variables
    aParse.leader = nil
    aParse.tag = nil
    aParse.body = nil
    aParse.tail = nil
    aParse.trailer = nil
    aParse.more = nil
    aParse.parts = nil

    -- Public Functions
    aParse.print = printOut
    aParse.size = size
    aParse.last = last
    aParse.leaf = leaf
    aParse.at = at
    aParse.text = text
    aParse.htmlToText = htmlToText
    aParse.unescape = unescape
    aParse.unescapeSmartQuotes = unescapeSmartQuotes
    aParse.normalizeLineBreaks = normalizeLineBreaks
    aParse.condenseWhitespace = condenseWhiteSpace
    aParse.addToBody = addToBody
    aParse.addToTag = addToTag
    aParse.footnote = footnote

    -- Constructors
    local function textTags(self, text, tags, level, offset)
        if tags == nil then tags = {"table", "tr", "td"} end
        if level == nil then level = 1 end
        if offset == nil then offset = 1 end
        local lc = string.lower(text)
        local startTag = string.find(lc, "<" .. tags[level])
        local endTag = string.find(lc, ">", startTag) + 1
        local startTail = findMatchingEndTag(lc, endTag, tags[level], offset)
        local endTail = string.find(lc, ">", startTail) + 1
        local startMore = string.find(lc, "<" .. tags[level], endTail)
        if startTag == nil or endTag == nil or
           startTail == nil or endTail == nil
        then
            -- parse exception
            error({message="Can't find tag: " .. tags[level], offset=offset})
        end


        self.leader = string.sub(text, 1, startTag - 1)
        self.tag = string.sub(text, startTag, endTag - 1)
        self.body = string.sub(text, endTag, startTail - 1)
        self.tail = string.sub(text, startTail, endTail - 1)
        self.trailer = string.sub(text, endTail)

        if level < #tags then
            self.parts = Parse(self.body, tags, level + 1, offset + endTag)
            self.body = nil
        else
            index = string.find(self.body, "<" .. tags[1])
            if index ~= nil then
                self.parts = Parse(self.body, tags, 0, offset + endTag)
                self.body = ""
            end
        end
        if startMore ~= nil then
            self.more = Parse(self.trailer, tags, level, offset+endTail)
            self.trailer = nil
        end

        return self
    end

    local function tagBody(self, tag, body, parts, more)
        self.leader = "\n"
        if tag == nil then tag = "nil" end
        self.tag = "<" .. tag .. ">"
        self.body = body
        self.tail = "</" .. tag .. ">"
        self.trailer = ""
        self.parts = parts
        self.more = more

        return self
    end

    -- Pick the right constructor, ewww
    if (type(textOrTag) == "string" or textOrTag == nil) and
       (type(tagsOrBody) == "string" or tagsOrBody == nil) and
       (type(levelOrParts) == "table" or levelOrParts == nil) and
       (type(offsetOrMore) == "table" or offsetOrMore == nil)
    then
        -- Parse(String tag, String body, Parse parts, Parse more)
        return tagBody(aParse, textOrTag, tagsOrBody,
                levelOrParts, offsetOrMore)

    elseif (type(textOrTag) == "string" or textOrTag == nil) and
           (type(tagsOrBody) == "table" or tagsOrBody == nil) and
           (type(levelOrParts) == "number" or levelOrParts == nil) and
           (type(offsetOrMore) == "number" or offsetOrMore == nil)
    then
        -- Parse(String text, String tags[], int leve, int offset)
        return textTags(aParse, textOrTag, tagsOrBody,
                levelOrParts, offsetOrMore)
    else
        msg = "Incorrect call to constructor:\n" ..
            "Parse("..type(textOrTag)..", "..type(tagsOrBody)..", "..
            type(levelOrParts) .. ", " .. type(offsetOrMore) .. ")"
        error(msg)
    end

end

setmetatable(_M, _M)
